package com.mzj.saas.commons.util;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.datatype.XMLGregorianCalendar;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTypeResolverBuilder;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.deser.ValueInstantiators.Base;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;

public class JsonUtils
{
	public static final ObjectMapper defaultObjectMapper = initAndGetDefaultObjectMapper() ;
	public static final ObjectMapper withTypeInfoObjectMapper = initAndGetWithTypeInfoObjectMapper() ;

	public static final JsonNode str2node(String str)
	{
		try
		{
			return defaultObjectMapper.readTree(str);
		}
		catch (IOException err)
		{
			throw new RuntimeException(err) ;
		}
	}

	public static final <T> T str2obj(String str, Class<T> clz)
	{
		try
		{
			return defaultObjectMapper.readValue(str, clz);
		}
		catch (IOException err)
		{
			throw new RuntimeException(err) ;
		}
	}

	public static final Object str2obj(String str, Map<String, Class<?>> clzs)
	{
		return str2obj(str, clzs, null);
	}

	public static final Object str2obj(String str, Map<String, Class<?>> clzs, Class<?> defaultClz)
	{
		try
		{
			JsonNode jsonNode = defaultObjectMapper.readTree(str);
			for (Map.Entry<String, Class<?>> entry : clzs.entrySet())
			{
				String fieldName = entry.getKey();
				Class<?> clz = entry.getValue();

				if (jsonNode.has(fieldName))
				{
					if (clz.isAssignableFrom(JsonNode.class))
						return jsonNode;
					else
						return defaultObjectMapper.treeToValue(jsonNode, clz);
				}
			}

			return defaultClz == null ? null : defaultObjectMapper.treeToValue(jsonNode, defaultClz);
		}
		catch (IOException err)
		{
			throw new RuntimeException(err) ;
		}
	}

	public static final <T> List<T> str2list(String str, Class<T> clz)
	{
		try
		{
			JavaType t = defaultObjectMapper.getTypeFactory().constructCollectionType(List.class, clz);
			return defaultObjectMapper.readValue(str, t);
		}
		catch (Exception err)
		{
			throw new RuntimeException(err) ;
		}
	}

	public static final String obj2str(Object obj, boolean withTypeInfo)
	{
		try
		{
			if( withTypeInfo )
				return withTypeInfoObjectMapper.writeValueAsString(obj);
			else
				return defaultObjectMapper.writeValueAsString(obj);
		}
		catch (Exception err)
		{
			throw new RuntimeException(err) ;
		}
	}

	public static final String obj2str(Object obj)
	{
		try
		{
			return withTypeInfoObjectMapper.writeValueAsString(obj);
		}
		catch (Exception err)
		{
			throw new RuntimeException(err) ;
		}
	}

	@SuppressWarnings("unchecked")
	public static final <T> T str2obj(String str)
	{
        try
		{
			return (T)withTypeInfoObjectMapper.readValue(str, Object.class);
		}
		catch (Exception err)
		{
			throw new RuntimeException(err) ;
		}
	}
	
	@SuppressWarnings("unchecked")
	public static final <T> T readObject(InputStream in)
	{
		try
		{
			return (T)withTypeInfoObjectMapper.readValue(in, Object.class) ;
		}
		catch (Exception err)
		{
			throw new RuntimeException(err) ;
		}
	}

	private static ObjectMapper initAndGetDefaultObjectMapper()
	{
		ObjectMapper mapper = new ObjectMapper() ;
		
		mapper.setAnnotationIntrospector( new AnnotationIntrospectorPair( new JaxbAnnotationIntrospector(mapper.getTypeFactory()), new JacksonAnnotationIntrospector() ) ) ;
		
		mapper.setSerializationInclusion(Include.NON_NULL);
		mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()
                .withFieldVisibility(JsonAutoDetect.Visibility.ANY).withGetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
		
		mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
		mapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
		mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
		mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
		
		return mapper ;
	}

	//Copy from JsonJacksonCodec of Redisson
	@SuppressWarnings("serial")
	private static ObjectMapper initAndGetWithTypeInfoObjectMapper()
	{
		ObjectMapper mapper = new ObjectMapper() ;
		mapper.registerModule(new DefenceModule());

		mapper.setAnnotationIntrospector( new AnnotationIntrospectorPair( new JaxbAnnotationIntrospector(mapper.getTypeFactory()), new JacksonAnnotationIntrospector() ) ) ;

		mapper.setSerializationInclusion(Include.NON_NULL);
		mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()
                .withFieldVisibility(JsonAutoDetect.Visibility.ANY).withGetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
		
		mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
		mapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
		mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
		mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
		
		mapper.addMixIn(Throwable.class, ThrowableMixIn.class);
		
		TypeResolverBuilder<?> mapTyper = new DefaultTypeResolverBuilder(DefaultTyping.NON_FINAL){
			public boolean useForType(JavaType t)
			{
				switch (_appliesFor)
				{
				case NON_CONCRETE_AND_ARRAYS:
					while (t.isArrayType())
						t = t.getContentType();
					// fall through
				case OBJECT_AND_NON_CONCRETE:
					return (t.getRawClass() == Object.class) || !t.isConcrete();
				case NON_FINAL:
					while (t.isArrayType())
						t = t.getContentType();

					// to fix problem with wrong long to int conversion
					if (t.getRawClass() == Long.class)
						return true;

					if (t.getRawClass() == XMLGregorianCalendar.class)
						return false;

					return !t.isFinal(); // includes Object.class
				default:
					// case JAVA_LANG_OBJECT:
					return (t.getRawClass() == Object.class);
				}
			}
		};

		mapTyper.init(JsonTypeInfo.Id.CLASS, null);
		mapTyper.inclusion(JsonTypeInfo.As.PROPERTY);
		
		mapper.setDefaultTyping(mapTyper);

		try
		{
			byte[] s = mapper.writeValueAsBytes(1);
			mapper.readValue(s, Object.class);
			
			return mapper ;
		}
		catch (IOException e)
		{
			throw new IllegalStateException(e);
		}
	}
	
    @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@id")
    @JsonAutoDetect(fieldVisibility = Visibility.NONE, getterVisibility = Visibility.PUBLIC_ONLY, setterVisibility = Visibility.PUBLIC_ONLY, isGetterVisibility = Visibility.PUBLIC_ONLY)
    public static class ThrowableMixIn 
    {
    }
	
	private static class DefenceModule extends SimpleModule 
	{
	    private static final long serialVersionUID = 1;

	    @Override
	    public void setupModule(SetupContext context) 
	    {
	        context.addValueInstantiators(new DefenceValueInstantiator());
	    }
	}
	
    private static class DefenceValueInstantiator extends Base 
    {
        protected final static Set<String> DEFAULT_NO_DESER_CLASS_NAMES;
        static 
        {
            Set<String> s = new HashSet<String>();
            // Courtesy of [https://github.com/kantega/notsoserial]:
            // (and wrt [databind#1599]
            s.add("org.apache.commons.collections.functors.InvokerTransformer");
            s.add("org.apache.commons.collections.functors.InstantiateTransformer");
            s.add("org.apache.commons.collections4.functors.InvokerTransformer");
            s.add("org.apache.commons.collections4.functors.InstantiateTransformer");
            s.add("org.codehaus.groovy.runtime.ConvertedClosure");
            s.add("org.codehaus.groovy.runtime.MethodClosure");
            s.add("org.springframework.beans.factory.ObjectFactory");
            s.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
            DEFAULT_NO_DESER_CLASS_NAMES = Collections.unmodifiableSet(s);
        }
        
        @Override
        public ValueInstantiator findValueInstantiator(DeserializationConfig config, BeanDescription beanDesc, ValueInstantiator defaultInstantiator) 
        {
            if (DEFAULT_NO_DESER_CLASS_NAMES.contains(beanDesc.getClassInfo().getRawType().getName()))
                throw new IllegalArgumentException("Illegal type " + beanDesc.getClassInfo().getRawType().getName() + " to deserialize: prevented for security reasons");
            
            return super.findValueInstantiator(config, beanDesc, defaultInstantiator);
        }
    }

}
